Ana içeriğe geç
  1. 100 Günde SwiftUI Notları/

37.Gün - SwiftUI: iExpense Uygulamasını İnşa Edelim

Bu bölümde @Observable , sheet() , Codable , UserDefaults kullanarak iExpense uygulamamızı inşa edeceğiz.

@Observable : Değişiklikler için bir sınıfı izler ve etkilenen tüm view’ları yeniler.

sheet() : Belirlediğimiz bir koşu izler ve bir view’ı otomatik olarak gösterir veya gizler.

Codable : Swift nesnelerini JSON’a dönüştürebilir.

UserDefaults : Verileri okuyabilir ve yazabilir, böylece ayarları ve daha fazlasını anında kaydedebiliriz.

Bu proje aynı zamanda GitHub’da da bulunmaktadır.

GitHub - GorkemGuray/iExpense: 100 Days of SwiftUI - Project-7

SwiftUI Satır Silinebilen bir Liste Oluşturma #

Bu projede bazı harcamaları gösterebilecek bir liste istiyoruz ve daha önce bunu bir @State array ile yapardık. Ancak burada farklı bir yaklaşım izleyeceğiz: @State kullanarak listemize eklenecek bir Expenses sınıfı oluşturacağız.

Öncelikle giderin ne olduğuna karar vermemiz gerekiyor. Bu örnekte üç şey olacak: giderin adı, iş veya kişisel olup olmadığı ve Double olarak maliyeti.

Buna daha sonra daha fazlasını ekleyeceğiz, ancak şimdilik tüm bunları tek bir ExpenseItem struct olarak kullanarak temsil edebiliriz. Bunu ExpenseItem.swift adında yeni bir Swift dosyasına koyabilirsiniz, ancak buna gerek yok.

Kullanmamız gerek kod;

struct ExpenseItem {
    let name: String
    let type: String
    let amount: Double
}

Artık tek bir gideri temsil eden bir şeyimiz olduğuna göre, bir sonraki adım bu gider kalemlerinin bir array’ini tek bir nesen içinde saklamak için bir şey oluşturmaktır. SwiftUI tarafından izlenebilmesi için bunun @Observable makrosunu kullanması gerekir.

ExpenseItem struct’ta olduğu gibi, bu da basit bir şekilde başlayacak ve daha sonra eklemeler yapacağız, bu yüzden bu yeni sınıfı şimdi ekleyelim;

@Observable
class Expenses {
    var items = [ExpenseItem]()
}

Böylece ana görünümümüz için gereken tüm veriler tamamlanmış oldu. Tek bir harcama kalemini temsil eden bir struct’ımız ve tüm bu kalemlerden oluşan bir array’i saklayan bir sınıfımız var.

Şimdi bunu SwiftUI view’larla eyleme geçirelim, böylece verilerimizi ekranda gerçekten görebiliriz. View’larımızın çoğu harcamalarımızdaki öğeleri gösteren bir List olacak, ancak kullanıcıların artık istemedikleri öğeleri silmelerini istediğimiz için sadece basit bir List kullanamayız. Listenin içinde bir ForEach kullanmamız gerekir, böylece onDelete() modifier’ına erişebiliriz.

İlk olarak, view’a Expenses sınıfımızın bir instance’ını oluşturacak bir @State property eklememiz gerekir.

@State private var expenses = Expenses()

Unutmayın, burada @State kullanmak nesneyi canlı tutar, ancak SwiftUI’ye herhangi bir değişiklik için nesneyi izleme gücünü veren aslında @Observable makrosudur.

İkinci olarak, temel düzenimizi oluşturmak için bu Expenses nesnesini bir NavigationStack, bir List ve bir ForEach ile birlikte kullanabiliriz.

NavigationStack {
    List {
        ForEach(expenses.items, id: \.name) { item in
            Text(item.name)
        }
    }
    .navigationTitle("iExpense")
}

Bu, ForEach’e her harcama kalemini adıyla benzersiz bir şekilde tanımlamasını söyler ve ardından adı liste satırı olarak yazdırır.

İşimiz bitmeden önce layout’a iki şey daha ekleyeceğiz: test amacıyla yeni öğeler ekleyebilme ve öğeleri kaydırarak silebilme.

Yakında kullanıcıların kendi öğelerini eklemelerine izin vereceğiz, ancak devam etmeden önce listemizin gerçekten iyi çalışıp çalışmadığını kontrol etmek önemlidir. Bu yüzden, örnek ExpenseItem instance ekleyen bir araç çubuğu butonu ekleyeceğiz, bu modifier’ı List’e ekelyelim.

.toolbar {
    Button("Add Expense", systemImage: "plus") {
        let expense = ExpenseItem(name: "Test", type: "Personal", amount: 5)
        expenses.items.append(expense)
    }
}

Bu, uygulamamızı hayat geçirir. Şimdi başlatabilir, ardından çok sayıda test gideri eklemek için + düğmesine tekrar tekrar basabiliriz.

Artık giderleri ekleyebildiğimize göre, bunları kaldırmak için de kod ekleyebiliriz. Bu, liste öğelerinden oluşan bir IndexSet, silebilen bir yöntem eklemek ve ardından bunu doğrudan giderler array’ine aktarmak anlamına gelir:

func removeItems(at offsets: IndexSet) {
    expenses.items.remove(atOffsets: offsets)
}

Bunu SwiftUI’ye eklemek için, ForEach ’e aşağıdaki gibi bir onDelete() modifier’ı ekliyoruz.

ForEach(expenses.items, id: \.name) { item in
    Text(item.name)
}
.onDelete(perform: removeItems)

Devam edin ve şimdi uygulamayı çalıştırın, birkaç kez + tuşuna basın, ardından satırları silmek için kaydırın.

id: \.name dediğimizde, her bir öğeyi ismiyle benzersiz bir şekilde tanımlayabileceğimizi söylüyoruz, ki bu burada doğru değil. Aynı isme birden çok kez sahibiz ve harcamalarımızın da benzersiz olacağını garanti edemeyiz.

Bu genellikle işe yarar, ancak bazen projenizde tuhaf, bozuk animasyonlara neden olur, bu yüzden şimdi daha iyi bir çözüme bakalım.

SwiftUI’de Identifiable Öğlerle Çalışma #

SwiftUI’de statik view’lar oluşturduğumuzda (yani bir VStack, bir TextField sonra bir Button vb. kodladığımızda) SwiftUI tam olarak hangi view’lara sahip olduğumuzu görebilir ve bunları kontrol edebilir, animate edebilir ve daha fazlasını yapabilir. Ancak dinamik view’lar oluşturmak için List veya ForEach kullandığımızda, SwiftUI’nin her bir öğeyi benzersiz bir şekilde nasıl tanımlayabileceğini bilmesi gerekir, aksi takdirde neyin değiştiğini anlamak için view hiyerarşilerini karşılaştırmakta zorlanacaktır.

Mevcut kodumuzda bu var;

ForEach(expenses.items, id: \.name) { item in
    Text(item.name)
}
.onDelete(perform: removeItems)

Yukarıdaki kod, gider kelemlerindeki her satır için benzersiz bir şekilde adıyla tanımlanan yeni bir satır oluşturmak, bu adı satırda göstermek ve silmek için removeItems() methodunu çağırmak anlamına gelir.

Daha sonra bu kodumuz var;

Button("Add Expense", systemImage: "plus") {
    let expense = ExpenseItem(name: "Test", type: "Personal", amount: 5)
    expenses.items.append(expense)
}

Bu butona her basıldığında, listemize bir test gideri ekler, böylece ekleme ve silmenin çalıştığından emin olabiliriz.

Her örnek gider kalemi oluşturduğumuzda “Test” adını kullanıyoruz, ancak SwiftUI’ye gider adını benzersiz bir tanımlayıcı olarak kullanabileceğini de söyledik. Dolayısıyla, kodumuz çalıştığında ve bir öğeyi sildiğimizde, SwiftUI önceden array’e bakar - “Test”, “Test”, “Test”, “Test” sonra tekrar array’e bakar “Test”, “Test” , “Test” ve neyin değiştiğini kolayca anlayamaz.

Bu durumda şanslıyız, çünkü List tam olarak hangi satırda kaydırma yaptığımızı biliyor, ancak diğer birçok yerde bu ekstra bilgi mevcut olmayacak ve uygulamamız garip davranmaya başlayacaktır.

Bu bizim adımıza bir mantık hatası: kodumuzda sorun yok ve çalışma zamanında çökmüyor, ancak bu sonuca ulaşmak için yanlış mantığı uyguladık ve SwiftUI’ye bir şeyin benzersiz olmadığı halde benzersiz bir tanımlayıcı olacağını söyledik.

Bunu düzeltmek için ExpenseItem struct hakkında daha fazla düşünmemiz gerekiyor. Şu anda üç property var: name, type ve amount . name pratike tek başına benzersiz olabilir, ancak muhtemelen ileride olmayacaktır. Kullanıcı iki kez “Lunch” girdiğinde sorunla karşılaşmaya başlayacağız.

Burada akıllı çözüm, ExpenseItem ’a atadığımız bir kimlik numarası gibi benzersiz bir şey eklemektir. Bu işe yarayacaktır, ancak atadığımız son numarayı takip etmek anlamına gelir.

Aslında daha kolay bir çözüm var ve buna UUID deniyor, Universally Unique Identifier (evrensel olarak benzersiz tanımlayıcı) ‘nın kısaltması.

08B15DB4-2F02-4AB8-A965-67A9C90D8A44 UUID’ler bunun gbi uzun hexadecimal stringler’dir. Yani sekiz hane, dört hane, dört hane ve on iki hane tek gereklilik üçüncü bloğun ilk sayısında bir 4 olmasıdır. Sabit 4’ü çıkarırsak, her biri 16 değerden biri olabilen 31 hane elde ederiz. Bu da bir milyar yıl boyunca her saniye 1 UUID üretseydik, küçük bir ihtimalle bir kopya üretebileceğimiz manasına gelirdi.

Şimdi, ExpanseItem ’i aşağıdaki gibi bir UUID property’ye sahip olacak şekilde güncelleyebiliriz.

struct ExpenseItem {
    let id: UUID
    let name: String
    let type: String
    let amount: Int
}

Ve bu işe yarayacaktır. Ancak aynı zamanda elle bir UUID oluşturmamız, ardından UUID’yi diğer verilerimizle birlikte yüklememiz ve kaydetmemiz gerektiği anlamına gelir. Bu yüzden, bu durumda Swift’ten bizim için otomatik olarak bir UUID oluşturmasını isteyeceğiz.

struct ExpenseItem {
    let id = UUID()
    let name: String
    let type: String
    let amount: Int
}

Artık expense item’ların id değerleri hakkında endişelenmemize gerek yok Swift bunların her zaman benzersiz olmasını sağlayacaktır.

Artık ForEach’i şu şekilde düzeltebiliriz.

ForEach(expenses.items, id: \.id) { item in
    Text(item.name)
}

Uygulamayı çalıştırırsanız sorunumuzun çözüldüğünü göreceksiniz.

Ancak bu adımla işimiz henüz bitmedi. Bunu yerine ExpanseItem ’ı Identifiable adlı yeni bir protokole uygun hale getirmek için aşağıdaki gibi değiştirelim

struct ExpenseItem: Identifiable {
    let id = UUID()
    let name: String
    let type: String
    let amount: Int
}

Yaptığımız tek şey protokol uyumlulukları listesine Identifiable ’ı eklemek. Bu Swift’te yerleşik olarak bulunan protokollerden biridir ve “bu tür benzersiz bir şekilde tanımlanabilir” anlamına gelir. Tek bir gereksinimi vardır, o da benzersiz bir tanımlayıcı içeren id adında bir property’nin olması gerektiğidir.

Şimdi expense item’ların bernzersiz bir şekilde tanımlanabileceği garanti edildiğinden, ForEach’e tanımlayıcı için hangi özelliği kullanacağını söylememize artık gerek yok.

Bu değişikliğin sonucu olarak ForEach ’i tekrar şu şekilde değiştirebiliriz.

ForEach(expenses.items) { item in
    Text(item.name)
}

Observed Bir Nesneyi Yeni Bir View’la Paylaşma #

@Observable kullanan sınıflar birden fazla SwiftUI view’da kullanılabilir ve sınıfın property’leri değiştiğinde tüm bu view’lar güncellenir. SwiftUI burada gerçekten akıllıdır: bu view’ları yalnızca değişen property’leri gerçekten kullanıyorsa güncelleyecektir.

Bu uygulamada, yeni gider kalemleri eklemek için özel olarak bir view tasarlayacağız. Kullanıcı hazır olduğunda, bunu Expenses sınıfımıza ekleyeceğiz ve bu da otomatik olarak orijinal view’ın verilerini yenilemesine neden olacak, böylece gider öğesi (expense item) gösterilebilecek.

Yeni bir SwiftUI view’ı oluşturmak için Cmd+N tuşlarına basabilir ya da File menüsüne gidip New>File ‘ı seçebilirsiniz. Her iki durumda da, User Interface kategorisi altında “SwiftUI View”’ını seçmeli ve ardından dosyaya AddView.swift adını vermelisiniz. Xcode size dosyayı nereye kaydedeceğini soracaktır, bu nedenle “iExpense” seçtiğinize emin olun, ardından Xcode’un size düzenlemeye hazır yeni view’ı göstermesi için Create’a tıklayın.

Diğer view’larda olduğu gibi, AddView’daki ilk işlemlerimiz basit olacak ve ardından eklemeler yapacağız. Bu expense name ve amount için text field’ların yanı sıra type için bir picker ekleyeceğimiz ve bunların tümünü bir form ve navigation stack ile saracağımız anlamına geliyor.

Şimdi aşağıdaki kodları inceleyelim;

struct AddView: View {
    @State private var name = ""
    @State private var type = "Personal"
    @State private var amount = 0.0

    let types = ["Business", "Personal"]

    var body: some View {
        NavigationStack {
            Form {
                TextField("Name", text: $name)

                Picker("Type", selection: $type) {
                    ForEach(types, id: \.self) {
                        Text($0)
                    }
                }

                TextField("Amount", value: $amount, format: .currency(code: "USD"))
                    .keyboardType(.decimalPad)
            }
            .navigationTitle("Add new expense")
        }
    }
}

Bu kodun geri kalanına birazdan geri döneceğiz, ancak önce ContentView’a biraz kod ekleyelim, böylece + butonuna dokunulduğunda AddView’i gösterebiliriz.

AddView’ı yeni bir view olarak sunmak için ContentView’da üç değişiklik yapmamız gerekiyor. İlk olarak, AddView’in gösterilip gösterilmediğini izlemek için bir state’e ihtiyacımız var, bu yüzden bunu şimdi bir property olarak ekleyin;

@State private var showingAddExpense = false

İkinci olarak, SwiftUI’ye bu Boolean’ı bir sheet olarak göstermek için bir koşul olarak kullanmasını söylememiz gerekir. Bu, sheet() modifier’ını view hiyerarşimizde bir yere ekleyerek yapılır. İsterseniz List’i kullanabilirsiniz, ancak NavigationStack de aynı şekilde çalışır. Her iki durumda da, bu kodu ContentView’deki view’lardan birine modifier olarak ekleyelim;

.sheet(isPresented: $showingAddExpense) {
    // show an AddView here
}

Üçüncü adım, sayfanın içine bir şey koymaktır. Genellikle bu sadece göstermek istediğimiz view türünün bir örneği olacaktır, bunun gibi;

.sheet(isPresented: $showingAddExpense) {
    AddView()
}

Ancak burada daha fazlasına ihtiyacımız var. Gördüğünüz gibi, content view’da zaten expenses property var ve AddView içinde expense item eklemek için kod yazacağız. AddView ’da Expenses sınıfının ikinci bir instance’ını oluşturmak istemiyoruz, bunun yerine ContentView’daki mevcut instance’ın paylaşılmasını istiyoruz.

Bu yüzden, yapacağımız şey AddView ’a bir Expenses nesnesini saklamak için bir property eklemektir. Bu property’yi AddView’a ekleyelim;

var expenses: Expensese

Ve şimdi mevcut Expenses nesnemizi bir view’dan diğerine aktarabiliriz - her ikisi de aynı nesneyi paylaşacak ve her ikisi de değişiklikleri izleyecektir. ContentView ’deki sheet() modifier’ını şu şekilde değiştirelim;

.sheet(isPresented: $showingAddExpense) {
    AddView(expenses: expenses)
}

İki nedenden dolayı bu adımı henüz tamamlamadık: kodumuz derlenmeyecek ve derlense bile butonumuz sayfayı tetiklemediği için çalışmayacaktır.

Derleme hatası, yeni SwiftUI view’ını oluşturduğumuzda Xcode’un kodlama sırasında view’ın tasarımına bakabilmemiz için bazı preview kodları da eklemesinden kaynaklanıyor. Bunu AddView.swift dosyasının en altında bulunan AddView instance’ının bir expenses property’si olmadan oluşturulmaya çalışıldığı için görürsünüz.

Bu durumu aşağıdaki gibi sahte bir değer geçerek atlatabiliriz.

#Preview
    AddView(expenses: Expenses())
}

İkinci sorun ise aslında sheet’i gösterecek herhangi bir kodumuzun olmamasıdır, çünkü şu anda ContentView’deki + butonu test harcamalarını eklemektedir. Mevcut action’ı showingaAddExpense Boolean’ı değiştirmek için şu kodla değiştirelim;

Button("Add Expense", systemImage: "plus") {
    showingAddExpense = true
}

Uygulamayı şimdi çalıştırırsanız, tüm sayfa amaçlandığı gibi çalışıyor olmalıdır. ContentView ile uygulama başlar, çeşitli alanları yazabileceğimiz bir AddView getirmek için + butonuna dokunuruz, ardından kapatmak için kaydırabiliriz.

UserDefaults ile Değişiklikleri Kalıcı Hale Getirme #

Bu noktada, uygulamamızın kullanıcı arayüzü işlevseldir, öğeleri ekleyip silebildiğimizi gördünüz ve şimdi yeni giderler oluşturmak için bir kullanıcı arayüzü gösteren bir sayfamız var. Ancak, uygulama çalışmaktan çok uzak: AddView’a yerleştirilen herhangi bir veri tamamen göz ardı ediliyor ve göz ardı edilmese bile uygulamanın ileride çalıştırılacağı zamanlar için kaydedilmiyor.

AddView ’den gelen verilerle gerçekten bir şeyler yapmakla başlayarak bu sorunları sırasıyla ele alacağız. Formumuzdaki değerleri saklayan property’lerimiz zaten var ve daha önce ContentView’den aktarılan bir Expenses nesnesini saklamak için bir property eklemiştik.

Bu iki şeyi bir araya getirmemiz gerekiyor: dokunulduğunda property’lerimizden bir ExpenseItem oluşturan ve bunu expenses öğelerine ekleyen bir butona ihtiyacımız var.

Bu modifier’ı AddView’da navigationTitle() methodunun altına ekleyelim;

.toolbar {
    Button("Save") {
        let item = ExpenseItem(name: name, type: type, amount: amount)
        expenses.items.append(item)
    }
}

Yapacak daha çok işimiz olmasına rağmen, uygulamayı şimdi çalıştırmanızı tavsiye ederim çünkü gerçekten bir araya geliyor. Artık AddView’ı gösterebilir, bazı ayrıntıları girebilir, “Save”’e basabilir, ardından kapatmak için kaydırabilir ve yeni öğenizi listede görebilirsiniz. Bu da veri senkronizasyonumuzun mükemmel çalıştığı anlamına geliyor. Her iki SwiftUI view’ı da aynı expense item’larından okuma yapıyor.

Şimdi uygulamayı tekrar başlatmayı deneyin ve hemen ikinci sorunumuzla karşılaşacaksınız. Eklediğiniz hiçbir veri saklanmaz, yani uygulamayı her yeniden başlattığınızda her şey boş başlar.

Bunun oldukça kötü bir kullanıcı deneyimi olduğu açık, ancak Expense ’i ayrı bir sınıf olarak kullanmamız sayesinde bunu düzeltmek aslında o kadar da zor değil.

Verileri temiz bir şekilde kaydetmemize ve yüklememize yardımcı olacak dört önemli teknolojiden yararlanacağız:

  1. Codable protokolü, mevcut tüm gider kalemlerini depolanmaya hazır şekilde arşivlememizi sağlayacak.
  2. UserDefaults , bu arşivlenmiş verileri kaydetmemize ve yüklememize izin verecektir.
  3. Expenses sınıfı için özel bir initiliazer, böylece bir instance’ını oluşturduğumuzda UserDefaults’dan kaydedilmiş verileri yükleriz.
  4. Expenses ’ın items property’si üzerinde bir didSet property observer’ı, böylece bir öğe eklendiğinde veya kaldırıldığında değişiklikleri yazacağız.

Önce verileri yazmayı ele alalım. Expenses sınıfında bu property’ye zaten sahibiz:

var items = [ExpenseItem]()

Oluşturulan tüm expense items struct’larını burada saklıyoruz ve değişiklikleri olduğu gibi yazmak için property observer’ı da buraya ekleyeceğiz.

Bu toplamda dört adımdan oluşur: verilerimizi JSON’a dönüştürme işini yapacak bir JSONEncoder instance oluşturmamız gerekir, ondan items array’i kodlamayı denemesini isteriz ve ardından bunu “Items” anahtarını kullanarak UserDefaults ’a yazabiliriz.

items property’yi şu şekilde değiştirelim;

var items = [ExpenseItem]() {
    didSet {
        if let encoded = try? JSONEncoder().encode(items) {
            UserDefaults.standard.set(encoded, forKey: "Items")
        }
    }
}

İpucu : JSONEncoder().encode() kullanmak , önce encoder’i oluşturup daha sonra kullanmak yerine tek adımda “bir encoder oluşturmak ve onu bir şeyi encode etmek için kullanmak” anlamına gelir.

Şimdi kodun derlenmediğini fark edeceksiniz. Sorun, encode() methodunun yalnızca Codable protokolüne uyan nesneleri arşivleyebilmesidir. Unutmayın, Codable ’a uygunluk, derleyiciden nesneleri arşivleme ve arşivden çıkarma işlemlerini yapabilmemiz için encode oluşturmasını isteyen şeydir ve bunun için bir uygunluk eklemezsek kodumuz oluşturulmayacaktır.

ExpenseItem ’a Codable eklemek dışında herhangi bir iş yapmamıza gerek yok:

struct ExpenseItem: Identifiable, Codable {
    let id = UUID()
    let name: String
    let type: String
    let amount: Int
}

Swift, ExpenseItem ’ın UUID, String ve Int property’leri için Codable uyumluluklarını zaten içeriyor ve bu nedenle ExpenseItem’ı istediğimiz anda otomatik olarak uyumlu hale getirebiliyor.

Ancak, id’nin çözülemeyeceğine dair bir uyarı göreceksiniz çünkü onu bir sabit yaptık ve varsayılan değer atadık. Aslında istediğimiz davranış bu, ancak Swift yardımcı olmaya çalışıyor çünkü bu değeri JSON’dan çözmeyi planlamış olabilirsiniz. Uyarıyı ortadan kaldırmak için property’yi şu şekilde değişken yapın:

var id = UUID()

Bu değişiklikle, kullanıcı eklendiğinde öğelerimizin kaydedildiğinden emin olmak için gereken tüm kodu yazdık. Ancak, bu tek başına etkili değildir. Verileri kaydedebilir, ancak uygulama yeniden başlatıldığında tekrar yüklenmez.

Bunu çözmek için aşağıdaki şekilde custom initializer uygulamamız gerekir.

  1. UserDefaults’dan “Items” anahtarını okumaya çalışın
  2. JSON verilerinden Swift nesnelerine geçmemizi sağlayan JSONEncoder’ın karşılığı olan bir JSONDecoder instance oluşturun.
  3. Decoder’den UserDefaults ’tan aldığımız verileri ExpenseItem nesneleri array’ine dönüştürmesini isteyin.
  4. Bu işe yaradıysa, elde edilen array’i items ’a atayın ve çıkın.
  5. Aksi takdirde, items ’ı boş bir array olarak ayarlayın.

Bu initializer’ı şimdi Expenses sınıfına ekleyin;

init() {
    if let savedItems = UserDefaults.standard.data(forKey: "Items") {
        if let decodedItems = try? JSONDecoder().decode([ExpenseItem].self, from: savedItems as! Data) {
            items = decodedItems
            return
        }
    }

    items = []
}

Bu kodun iki önemli kısmı vardır.

  • .data(forKey: "Items") satırı “Items” anahtarında ne varsa Data nesnesi olarak okumaya çalışır.
  • try? JSONDecoder().decode([ExpenseItem].self, from: savedItems as! Data) satırı Data nesnesini ExpenseItem nesnelerinden oluşan bir array’e açma işini gerçekleştirir.

[ExpenseItem].self ’i ilk gördüğünüzde biraz tereddüt etmeniz normaldir, .self ne anlama geliyor? Eğer sadece [ExpenseItem] kullanmış olsaydık, Swift ne demek istediğimizi bilmek isteyecekti - sınıfın bir kopyasını mı oluşturmaya çalışıyoruz? Statik bir property veya methoda başvurmayı mı planlıyorduk? Sınıfın bir instance’ını mı oluşturmak istiyorduk? Karışıklığı önlemek için - tür nesnesi olarak bilinen türün kendisine atıfta bulunduğumuzu söylemek için- ardından .self yazıyoruz.

Son Dokunuşlar #

Uygulamayı kullanmayı denerseniz, kısa süre içinde iki sorunu olduğunu göreceksiniz;

  1. Bir gider eklediğinizde, bununla ilgili herhangi bir ayrıntı göremezsiniz.
  2. Bir masraf eklemek AddView’i kapatmaz, orada kalır.

Bu projeyi tamamlamadan önce, her şeyi biraz daha iyi hissettirmek için bunları düzeltelim.

İlk olarak, AddView’ın kapatılması, doğru zaman geldiğinde environment üzerinde dismiss() çağrısı yapılarak gerçekleştirilir. Bu, view’ın environment’i tarafından kontrol edilir ve sayfamız için isPresented parametresine bağlanır. AddView ’i göstermek için bu Boolean bizim tarafımızdan true olarak ayarlanır, ancak dismiss() methodunu çağırdığımızda environment tarafından false değerine geri çevrilir.

Bu property’yi AddView’e ekleyerek başlayalım;

@Environment(\.dismiss) var dismiss

Bunun için bir tür belirtmediğimizi fark edeceksiniz, Swift @Environment property wrapper sayesinde bunu anlayabilir.

Daha sonra, view’ın kendisini kapatmasını istediğimizde dismiss() methodunu çağırmamız gerekir. Bu ContentView’deki showingAddExpense Boolean’ının false değerine geri dönmesine neden olur ve AddView’i gizler. AddView’de yeni bir gider kalemi oluşturan ve bunu mevcut giderlerimize ekleyen bir Save butonu zaten var, bu yüzden bunu hemen sonraki satıra ekleyelim;

dismiss()

Bu ilk sorunu çözer, geriye ikinci sorun kalır: her bir harcama kaleminin adını gösteririz ama başka bir şey göstermeyiz. Bunun nedeni aşağıdaki kod bloğudur.

ForEach(expenses.items) { item in
    Text(item.name)
}

Tüm bilgilerin ekranda iyi göründüğünden emin olmak için bunu başka bir Stack içinde Stack ile değiştireceğiz. Bu tür bir düzen iOS’ta yaygındır: solda başlık ve alt başlık, sağda ise daha fazla bilgi.

ContentView’deki mevcut ForEach’i bununla değiştirelim;

ForEach(expenses.items) { item in
    HStack {
        VStack(alignment: .leading) {
            Text(item.name)
                .font(.headline)
            Text(item.type)
        }

        Spacer()
        Text(item.amount, format: .currency(code: "USD"))
    }
}

Şimdi uygulamayı son bir kez çalıştırın ve deneyin.


Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.

Bu yazı, SwiftUI Day 37 adresinde bulunan yazılardan kendim için aldığım notları içermektedir. Orjinal dersi takip etmek için lütfen bağlantıya tıklayın.